<?php
/**
 * Zend Framework
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 * @category Zend
 * @package Zend_Loader
 * @subpackage Autoloader
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @version $Id: Resource.php 24593 2012-01-05 20:35:02Z matthew $
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
/**
 * Zend_Loader_Autoloader_Interface
 */
require_once 'Zend/Loader/Autoloader/Interface.php';

/**
 * Resource loader
 * @uses Zend_Loader_Autoloader_Interface
 * @package Zend_Loader
 * @subpackage Autoloader
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
class Zend_Loader_Autoloader_Resource implements Zend_Loader_Autoloader_Interface {

    /**
     *
     * @var string Base path to resource classes
     */
    protected $_basePath;

    /**
     *
     * @var array Components handled within this resource
     */
    protected $_components = array();

    /**
     *
     * @var string Default resource/component to use when using object registry
     */
    protected $_defaultResourceType;

    /**
     *
     * @var string Namespace of classes within this resource
     */
    protected $_namespace;

    /**
     *
     * @var array Available resource types handled by this resource autoloader
     */
    protected $_resourceTypes = array();

    /**
     * Constructor
     * @param array|Zend_Config $options Configuration options for resource autoloader
     * @return void
     */
    public function __construct ($options) {
        if ($options instanceof Zend_Config) {
            $options = $options -> toArray ();
        }
        if ( ! is_array ($options)) {
            require_once 'Zend/Loader/Exception.php';
            throw new Zend_Loader_Exception ('Options must be passed to resource loader constructor');
        }
        $this -> setOptions ($options);
        $namespace = $this -> getNamespace ();
        if ((null === $namespace) || (null === $this -> getBasePath ())) {
            require_once 'Zend/Loader/Exception.php';
            throw new Zend_Loader_Exception ('Resource loader requires both a namespace and a base path for initialization');
        }
        if ( ! empty ($namespace)) {
            $namespace .= '_';
        }
        require_once 'Zend/Loader/Autoloader.php';
        Zend_Loader_Autoloader::getInstance () -> unshiftAutoloader ($this, $namespace);
    }

    /**
     * Overloading: methods
     * Allow retrieving concrete resource object instances using 'get<Resourcename>()'
     * syntax.
     * Example:
     * <code>
     * $loader = new Zend_Loader_Autoloader_Resource(array(
     * 'namespace' => 'Stuff_',
     * 'basePath' => '/path/to/some/stuff',
     * ))
     * $loader->addResourceType('Model', 'models', 'Model');
     * $foo = $loader->getModel('Foo'); // get instance of Stuff_Model_Foo class
     * </code>
     * @param string $method
     * @param array $args
     * @return mixed
     * @throws Zend_Loader_Exception if method not beginning with 'get' or not matching a valid resource type is called
     */
    public function __call ($method, $args) {
        if ('get' == substr ($method, 0, 3)) {
            $type = strtolower (substr ($method, 3));
            if ( ! $this -> hasResourceType ($type)) {
                require_once 'Zend/Loader/Exception.php';
                throw new Zend_Loader_Exception ("Invalid resource type $type; cannot load resource");
            }
            if (empty ($args)) {
                require_once 'Zend/Loader/Exception.php';
                throw new Zend_Loader_Exception ("Cannot load resources; no resource specified");
            }
            $resource = array_shift ($args);
            return $this -> load ($resource, $type);
        }
        require_once 'Zend/Loader/Exception.php';
        throw new Zend_Loader_Exception ("Method '$method' is not supported");
    }

    /**
     * Helper method to calculate the correct class path
     * @param string $class
     * @return False if not matched other wise the correct path
     */
    public function getClassPath ($class) {
        $segments = explode ('_', $class);
        $namespaceTopLevel = $this -> getNamespace ();
        $namespace = '';
        if ( ! empty ($namespaceTopLevel)) {
            $namespace = array();
            $topLevelSegments = count (explode ('_', $namespaceTopLevel));
            for ($i = 0; $i < $topLevelSegments; $i ++ ) {
                $namespace[] = array_shift ($segments);
            }
            $namespace = implode ('_', $namespace);
            if ($namespace != $namespaceTopLevel) {
                // wrong prefix? we're done
                return false;
            }
        }
        if (count ($segments) < 2) {
            // assumes all resources have a component and class name, minimum
            return false;
        }
        $final = array_pop ($segments);
        $component = $namespace;
        $lastMatch = false;
        do {
            $segment = array_shift ($segments);
            $component .= empty ($component) ? $segment : '_' . $segment;
            if (isset ($this -> _components[$component])) {
                $lastMatch = $component;
            }
        } while (count ($segments));
        if ( ! $lastMatch) {
            return false;
        }
        $final = substr ($class, strlen ($lastMatch) + 1);
        $path = $this -> _components[$lastMatch];
        $classPath = $path . '/' . str_replace ('_', '/', $final) . '.php';
        if (Zend_Loader::isReadable ($classPath)) {
            return $classPath;
        }
        return false;
    }

    /**
     * Attempt to autoload a class
     * @param string $class
     * @return mixed False if not matched, otherwise result if include operation
     */
    public function autoload ($class) {
        $classPath = $this -> getClassPath ($class);
        if (false !== $classPath) {
            return include $classPath;
        }
        return false;
    }

    /**
     * Set class state from options
     * @param array $options
     * @return Zend_Loader_Autoloader_Resource
     */
    public function setOptions (array $options) {
        // Set namespace first, see ZF-10836
        if (isset ($options['namespace'])) {
            $this -> setNamespace ($options['namespace']);
            unset ($options['namespace']);
        }
        $methods = get_class_methods ($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst ($key);
            if (in_array ($method, $methods)) {
                $this -> $method ($value);
            }
        }
        return $this;
    }

    /**
     * Set namespace that this autoloader handles
     * @param string $namespace
     * @return Zend_Loader_Autoloader_Resource
     */
    public function setNamespace ($namespace) {
        $this -> _namespace = rtrim ((string) $namespace, '_');
        return $this;
    }

    /**
     * Get namespace this autoloader handles
     * @return string
     */
    public function getNamespace () {
        return $this -> _namespace;
    }

    /**
     * Set base path for this set of resources
     * @param string $path
     * @return Zend_Loader_Autoloader_Resource
     */
    public function setBasePath ($path) {
        $this -> _basePath = (string) $path;
        return $this;
    }

    /**
     * Get base path to this set of resources
     * @return string
     */
    public function getBasePath () {
        return $this -> _basePath;
    }

    /**
     * Add resource type
     * @param string $type identifier for the resource type being loaded
     * @param string $path path relative to resource base path containing the resource types
     * @param null|string $namespace sub-component namespace to append to base namespace that qualifies this resource type
     * @return Zend_Loader_Autoloader_Resource
     */
    public function addResourceType ($type, $path, $namespace = null) {
        $type = strtolower ($type);
        if ( ! isset ($this -> _resourceTypes[$type])) {
            if (null === $namespace) {
                require_once 'Zend/Loader/Exception.php';
                throw new Zend_Loader_Exception ('Initial definition of a resource type must include a namespace');
            }
            $namespaceTopLevel = $this -> getNamespace ();
            $namespace = ucfirst (trim ($namespace, '_'));
            $this -> _resourceTypes[$type] = array('namespace' => empty ($namespaceTopLevel) ? $namespace : $namespaceTopLevel . '_' . $namespace);
        }
        if ( ! is_string ($path)) {
            require_once 'Zend/Loader/Exception.php';
            throw new Zend_Loader_Exception ('Invalid path specification provided; must be string');
        }
        $this -> _resourceTypes[$type]['path'] = $this -> getBasePath () . '/' . rtrim ($path, '\/');
        $component = $this -> _resourceTypes[$type]['namespace'];
        $this -> _components[$component] = $this -> _resourceTypes[$type]['path'];
        return $this;
    }

    /**
     * Add multiple resources at once
     * $types should be an associative array of resource type => specification
     * pairs.
     * Each specification should be an associative array containing
     * minimally the 'path' key (specifying the path relative to the resource
     * base path) and optionally the 'namespace' key (indicating the subcomponent
     * namespace to append to the resource namespace).
     * As an example:
     * <code>
     * $loader->addResourceTypes(array(
     * 'model' => array(
     * 'path' => 'models',
     * 'namespace' => 'Model',
     * ),
     * 'form' => array(
     * 'path' => 'forms',
     * 'namespace' => 'Form',
     * ),
     * ));
     * </code>
     * @param array $types
     * @return Zend_Loader_Autoloader_Resource
     */
    public function addResourceTypes (array $types) {
        foreach ($types as $type => $spec) {
            if ( ! is_array ($spec)) {
                require_once 'Zend/Loader/Exception.php';
                throw new Zend_Loader_Exception ('addResourceTypes() expects an array of arrays');
            }
            if ( ! isset ($spec['path'])) {
                require_once 'Zend/Loader/Exception.php';
                throw new Zend_Loader_Exception ('addResourceTypes() expects each array to include a paths element');
            }
            $paths = $spec['path'];
            $namespace = null;
            if (isset ($spec['namespace'])) {
                $namespace = $spec['namespace'];
            }
            $this -> addResourceType ($type, $paths, $namespace);
        }
        return $this;
    }

    /**
     * Overwrite existing and set multiple resource types at once
     * @see Zend_Loader_Autoloader_Resource::addResourceTypes()
     * @param array $types
     * @return Zend_Loader_Autoloader_Resource
     */
    public function setResourceTypes (array $types) {
        $this -> clearResourceTypes ();
        return $this -> addResourceTypes ($types);
    }

    /**
     * Retrieve resource type mappings
     * @return array
     */
    public function getResourceTypes () {
        return $this -> _resourceTypes;
    }

    /**
     * Is the requested resource type defined?
     * @param string $type
     * @return bool
     */
    public function hasResourceType ($type) {
        return isset ($this -> _resourceTypes[$type]);
    }

    /**
     * Remove the requested resource type
     * @param string $type
     * @return Zend_Loader_Autoloader_Resource
     */
    public function removeResourceType ($type) {
        if ($this -> hasResourceType ($type)) {
            $namespace = $this -> _resourceTypes[$type]['namespace'];
            unset ($this -> _components[$namespace]);
            unset ($this -> _resourceTypes[$type]);
        }
        return $this;
    }

    /**
     * Clear all resource types
     * @return Zend_Loader_Autoloader_Resource
     */
    public function clearResourceTypes () {
        $this -> _resourceTypes = array();
        $this -> _components = array();
        return $this;
    }

    /**
     * Set default resource type to use when calling load()
     * @param string $type
     * @return Zend_Loader_Autoloader_Resource
     */
    public function setDefaultResourceType ($type) {
        if ($this -> hasResourceType ($type)) {
            $this -> _defaultResourceType = $type;
        }
        return $this;
    }

    /**
     * Get default resource type to use when calling load()
     * @return string null
     */
    public function getDefaultResourceType () {
        return $this -> _defaultResourceType;
    }

    /**
     * Object registry and factory
     * Loads the requested resource of type $type (or uses the default resource
     * type if none provided).
     * If the resource has been loaded previously,
     * returns the previous instance; otherwise, instantiates it.
     * @param string $resource
     * @param string $type
     * @return object
     * @throws Zend_Loader_Exception if resource type not specified or invalid
     */
    public function load ($resource, $type = null) {
        if (null === $type) {
            $type = $this -> getDefaultResourceType ();
            if (empty ($type)) {
                require_once 'Zend/Loader/Exception.php';
                throw new Zend_Loader_Exception ('No resource type specified');
            }
        }
        if ( ! $this -> hasResourceType ($type)) {
            require_once 'Zend/Loader/Exception.php';
            throw new Zend_Loader_Exception ('Invalid resource type specified');
        }
        $namespace = $this -> _resourceTypes[$type]['namespace'];
        $class = $namespace . '_' . ucfirst ($resource);
        if ( ! isset ($this -> _resources[$class])) {
            $this -> _resources[$class] = new $class ();
        }
        return $this -> _resources[$class];
    }

}
